认识 Linux 内存构成:Linux 内存调优之内存分配机制和换页行为认知
99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式
写在前面
- 博文内容涉及 Linux 中内存分配和换页机制的基本认知
- 理解不足小伙伴帮忙指正 :),生活加油
99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式
前面的文章和小伙伴们分享了 Linux 虚拟内存,物理内存,以及页表,TLB,大页认知,今天我们来看看具体的内存分配以及换页行为
内存分配机制和换页行为认知
内存分配机制
前面的博文我们有讲到,Linux 系统中进程内存的使用是通过申请虚拟内存,按需分配物理内存的方式
,内存页
是内存的基本单位,Linux 一个标准的内存页一般为 4kb, 具体要由 CPU 确定,虚拟内存地址
和物理内存地址
通过页表
来建立映射关系,页表是由多个页表项
构成,一个内存页
对应一个页表项
,所以映射会造成会有一个巨大的页表
,所以一般系统会使用多级页表
的方式按需建立映射关系,类似文章的四级目录一样,进程在每次访问内存需要查询页表,找到物理地址加上偏移量,获取实际的物理地址,在多级页表的情况下因为会查询多个表,所以会有延迟,所以会通过TLB( CPU 硬件缓存)
来缓存高频的页表项,每次访问页表先查询 TLB
,所以TLB的命中率
会直接影响查询效率。
同时,Linux 提供大页
支持,包含标准大页
和透明大页
,透明大页是内核自动管理,用于合并多个标准内存页,标准大页需要手动预分配连续的物理内存,用于减少页表层级。提高查询效率,当页表查询没有映射物理内存页时,会触发缺页异常
,缺页异常会分配物理内存(按需分配)写入页表建立映射关系,同时写入TLB
。
今天我们来看一下系统中进程 申请内存是如何发生的
?
下面是一个典型的内存页的生命周期
1.进程申请虚拟内存的过程是内存分配器与内核协同完成的机制
,对使用 glibc
内存分配器的进程来说,glibc
提供了一系列内存分配的函数
,包括 malloc() 和 free()等
,应用程序发起内存分配请求会调用相关的内核方法(例如,libc malloc())。
在Linux系统中,malloc()、free()、realloc()和calloc()是常用的内存分配和释放函数,主要用于动态内存管理。
malloc()
用于分配指定大小的内存块,并返回指向该内存的指针。如果分配失败,则返回NULL。free()
用于释放之前通过malloc()、calloc()或realloc()分配的内存,以避免内存泄漏。realloc()
用于调整已分配内存块的大小,可以扩大或缩小内存块,若无法满足新大小的要求,可能会返回NULL。calloc()
不仅分配内存,还会将其初始化为零,适用于需要初始化内存的场景
这些函数通常依赖于底层的系统调用如brk()和mmap()
来管理虚拟内存,当进程调用 malloc()
等函数时,glibc
首先在进程的用户虚拟地址空间
中划分一块连续的虚拟内存区域
。 此时仅进行逻辑分配,不涉及物理内存占用,当实际需要物理内存时,会通过触发缺页异常映射物理内存。
2.这块连续的虚拟内存区域,对于小内存分配
,优先从 brk()
扩展的堆区
分配,利用空闲链表
优化碎片。对于大内存分配
,直接调用 mmap()
映射独立段
,避免堆区碎片化
。
小内存分配:优先从 brk()
对于 brk()
的方式,会直接从空闲列表
中响应请求,在应用程序启动时,动态内存分配库会根据堆内存的初始状态初始化空闲链表,首次分配时
,若程序未显式初始化堆
,首次调用 malloc()
或 calloc()
时会通过 brk()
扩展堆空间,并初始化空闲链表
管理堆内存块
。某些场景下,需要显示初始化,需手动初始化链表头指针为空(head = NULL),表示空链表,例如 C 语言中的链表构造
空闲列表(Free List)用于动态管理
堆内存中的空闲内存块
。其本质是通过链表形式
记录所有未被占用的内存区域,实现内存的高效分配与回收,内核为每个CPU 和 DRAM
组维护一组空闲内存列表(freelist)
同时,内核软件
本身的内存分配需求也从这个空闲内存列表直接获取
,一般通过内核内存分配器
进行,例如,slab 分配器
。
只有在内存用尽时才需要扩展堆内存
,当空闲列表耗尽时,通过 brk
系统调用上移堆顶指针,扩大虚拟地址空间
,所以 brk 的优点是批量扩展堆空间,可以减少用户态与内核态的切换,缺点是
释放内存时会将被释放内存的地址记录下来
,以便提供给未来的malloc()
使用。通过空闲列表缓存释放的内存块,避免频繁调用系统调用。
对于小块内存(<128KB):释放后挂入 fastbins
或 small bins
,供后续 malloc
快速复用。fastbins
不合并相邻块以提升效率,而 small bins
则按固定大小分类管理(fastbins等为空闲链表数据结构)。
默认仅在堆顶存在连续空闲内存且超过阈值(默认128KB)时,调用 malloc_trim
通过 brk
下移堆顶归还内存
。频繁收缩会因系统调用开销影响性能,因此倾向于保留虚拟内存
。
虚拟内存的惰性回收,管理的堆内存本质上是进程的虚拟地址空间,其释放仅标记为可复用,不立即归还物理内存。物理内存的回收由内核的页框管理算法决定,例如通过缺页中断按需分配、通过 kswapd
内核线程异步回收。保留虚拟内存池避免了重复的 brk
调用,减少了用户态与内核态的切换开销,尤其在高并发场景下显著提升效率。
如果希望强制内存归还
,调用 malloc_trim(0)
可立即触发堆收缩,将空闲内存归还操作系统。这在需要严格控制内存占用的场景(如嵌入式系统)
中尤为重要。
大内存分配:直接调用 mmap()
匿名内存映射,mmap()
创建独立的内存段(如 MAP_ANONYMOUS),不依赖堆区
,直接映射到进程虚拟地址空间
,一般直接按页对齐分配(对应内存页页的整数倍)
,直接建立虚拟地址到物理内存的映射。
mmap()
既可以使用标准的 4KB 内存页
,也可以使用 静态大页(HugePages)
,如果进程使用的是 mmap()
系统函数调用,则必须挂载-个 hugetlbfs
文件系统。
1 | ┌──[root@liruilongs.github.io]-[~] |
mmap
创建的内存段可单独释放(通过 munmap
),避免碎片,归还操作系统。
3.内存分配之后,应用程序试图使用 store/load
指令来使用之前分配的内存地址,这就要调用 CPU 内部的内存管理单元(MMU)来进行虚拟地址到物理地址的转换
。会发现页表里面没有对应的页表项,MMU 触发缺页错误(page fault)。
4.缺页错误由系统内核处理
。在对应的处理函数中,内核会在物理内存空闲列表
中找到一个空闲地址
并映射到该虚拟地址。会把映射关系写入 MMU
,并且更新TLB,
到这里该用户进程就占据了一个新的物理内存页。进程所使用的全部物理内存数量称为常驻集大小(Resident set Size,RSS)
。
ps 命令的 RSS 列
1 | ┌──[root@liruilongs.github.io]-[~] |
top 命令的 RES 列
1 | ┌──[root@liruilongs.github.io]-[~] |
换页机制
当系统内存需求超过一定水平时,内核中的页换出守护进程(kswapd)
就开始寻找可以释放的内存页. 类似一些编程语言的垃圾回收
1 | [root@liruilongs.github.io ~]# ps -eaf | grep ksw | grep -v col |
当系统内存剩余量低于低水位阈值(pages_low)
时,内核的页换出守护进程(kswapd)
会被唤醒并开始回收内存。
守护进程(kswapd)
会释放以下列出的三种内存页之一:
文件系统页
:从磁盘中读出并且是没有修改过的页(术语为有磁盘备份的页(backed by disk),这些页可以立即被释放
,等需要的时候可以再读取回来这些页包括应用程序可执行代码、数据,以及文件系统的元数据
等。被修改过的文件系统页
:这些页被称为脏页
,这些页需要先写回磁盘才能被释放
。应用程序内存页
:这些页被称为匿名页(anonymous memory)
,因为这些页不是来源于某个文件的。如果系统中有换页设备(swap device)
,那么这些页可以先存入换页设备,再被释放
。将内存页写入换页设备(在 linux系统上)称为换页
。
这一过程的触发水位和调控机制与以下核心概念和参数相关:
Linux 内核通过三个 内存水位阈值(watermark) 衡量内存压力,并决定是否触发 kswapd 的回收行为
:
页高阈值(pages_high)
:当剩余内存(pages_free
)高于此值,表示内存充足,kswapd 处于休眠状态。页低阈值(pages_low)
:当剩余内存 介于页低阈值和页高阈值之间,内存有一定压力,但 kswapd 不会主动回收。当剩余内存 低于此值,kswapd 被唤醒并开始回收内存,直到剩余内存超过页高阈值。页最小阈值(pages_min)
,当剩余内存 低于此值,系统内存极度紧张,触发 直接内存回收(同步阻塞进程)或 OOM Killer 杀死进程。
这三个阈值通过以下公式动态计算(基于 min_free_kbytes
):
[
\text{pages_low} = \text{pages_min} \times \frac{5}{4}, \quad \text{pages_high} = \text{pages_min} \times \frac{3}{2}
]
min_free_kbytes
(页最小阈值的直接控制): 设置系统保留的最小空闲内存(单位为 KB),直接影响 pages_min
的值。
1 | echo 65536 > /proc/sys/vm/min_free_kbytes # 设置为 64MB |
注意事项:
- 值过高会导致内存浪费,过低可能频繁触发直接内存回收。
- 默认值通常为系统总内存的 0.1%~3%,需根据实际负载调整。
可以通过 /proc/zoneinfo
查看各内存区域的 min
、low
、high
值。
1 | [root@liruilongs.github.io ~]# cat /proc/zoneinfo | grep -A 5 'pages free' |
守护进程(kswapd)
会被定期唤醒,它会批量扫描活跃页的LRU列表
和非活跃页的LRU列表
以寻找可以释放的内存。当空闲内存低于某个阈值的时候,该进程就会被唤醒,当空闲内存高于另外一个阌值时才会休息,
kswapd
负责协调在后台进行页换出操作;除非CPU和磁盘IO极为紧张,否则这些操作不会影响应用程序的性能。如果kswapd释放内存的速度不够快,导致页数量低于系统中配置的最低页数量,那么它就会切换到直接回收模式
在这种模式下,页回收会直接在前台运行,直接释放内存以便应对新的内存分配请求
。在这种模式中,内存分配会阻塞直到有新的页被释放为止
在直接回收模式下,kswapd 可以直接调用内核模块的收缩(shrinker)函数
,这些函数释放的内存很有可能来自内核的缓存区域
,包括内核中的slab缓存
。
对于上面应用程序的换页操作,交换分区的使用一般会导致应用程序运行速度大幅下降。所以一般的生产系统根本不会配置换页设备,在没有配置换页设备的系统出现内存不足的情况时,内核会调用内存溢出进程终止程序杀掉某个进程。为了避免被杀掉,应用程序应该配置为避免超出系统内存的上限(Cgroup)
。
缓存/缓冲区机制
当内存不够时
,Linux 会进行换页操作
,在极端情况下会直接杀掉内存溢出进程的 OOM Killer
,在内存充足
的时候, Linux 会使用空闲的内存
来作为文件系统的缓存
,cache
解决读延迟,buffer
解决写的延迟。
虽然被使用了,但是这部分内存实际是可以使用的,可用内存: available ≈ free + buffers + cache(可回收部分)
在 Linux 中,可以通过 换页操作(vm.swappniess)
来调整是优先释放文件系统缓存还是优先进行其他的内存释放
操作。
1 | echo 0 > /proc/sys/vm/swappiness |
决定内核优先回收 匿名页(交换到 Swap) 还是 文件页(直接释放) 的倾向。0(优先回收文件页)到 100(积极使用 Swap)。默认值:60,建议对延迟敏感场景设为 0(k8s 部署会禁用交换分区)
也可以通过命令主动释放缓存和缓冲的内存占用
,需要注意的是在释放需要把数据写回磁盘
1 | ┌──[root@liruilongs.github.io]-[/proc/sys/vm] |
博文部分内容参考
© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)
《BPF Performance Tools》
《性能之巅 系统、企业与云可观测性(第2版)》
《 Red Hat Performance Tuning 442 》
© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)
认识 Linux 内存构成:Linux 内存调优之内存分配机制和换页行为认知
https://liruilongs.github.io/2024/06/16/待发布/认识 Linux 内存构成:Linux 内存调优之内存分配机制和换页行为认知/